﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Data;
using System.Data.OleDb;
using System.Linq;
using System.Reflection;

using ADOX;

namespace EasyORM
{
	public class DataStorage
	{
		private const string fileName = "db";
		private const string fileExtension = ".mdb";
		private const string pnKey = "Key";
		private const string pref = "d";
		
		private readonly Dictionary<object, DataRow> rowsDict = new Dictionary<object, DataRow>();
		private readonly Dictionary<Type, DictionaryItem> dictionary;
		private readonly DataSet dataSet = new DataSet();
		private int counter = 0;
		
		private struct DictionaryItem
		{
			public OleDbDataAdapter Adapter { get; set; }
			public INotifyCollectionChanged Collection { get; set; }
			public string TableName { get; set; }
		}
		
		private static string GetConnectionString(string dbName, string password)
		{
			return "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=" +
				dbName + fileExtension +
				";Jet OLEDB:Database Password=" +
				password;
		}
		
		public static DataStorage GetDataStorage(string dbName, string password, Type[] types) {
			return new DataStorage(dbName, password, types);
		}
		
		public static ObservableCollection<T> GetSingleCollection<T>() {
			return (new DataStorage(fileName, String.Empty, new Type[] { typeof(T) }))
				.GetCollection<T>();
		}

		private DataStorage(string dbName, string password, Type[] types)
		{
			bool exist = System.IO.File.Exists(dbName + fileExtension);
			
			if (!exist) Create(dbName, password, types);
			
			dictionary = types.ToDictionary(type => type,
			                                type => {
			                                	var tableName = pref + type.Name;
			                                	
			                                	OleDbDataAdapter adapter = new OleDbDataAdapter(
			                                		new OleDbCommand() {
			                                			CommandType = CommandType.TableDirect,
			                                			CommandText = tableName,
			                                			Connection = new OleDbConnection(GetConnectionString(dbName, password)) });
			                                	
			                                	OleDbCommandBuilder builder = new OleDbCommandBuilder(adapter);
			                                	
			                                	adapter.DeleteCommand = builder.GetDeleteCommand();
			                                	adapter.InsertCommand = builder.GetInsertCommand();
			                                	adapter.UpdateCommand = builder.GetUpdateCommand();
			                                	
			                                	var collection = (INotifyCollectionChanged)typeof(ObservableCollection<>)
			                                		.MakeGenericType(type)
			                                		.GetConstructor(new Type[]{})
			                                		.Invoke(null);
			                                	
			                                	collection.CollectionChanged += CollectionChanged;

			                                	return new DictionaryItem() {
			                                		TableName = tableName,
			                                		Adapter = adapter,
			                                		Collection = collection
			                                	};
			                                });
			
			dataSet.Tables.AddRange(types
			                        .Select(type => new DataTable(dictionary[type].TableName))
			                        .ToArray());
			Open();
		}

		private void CollectionChanged(object sender, NotifyCollectionChangedEventArgs args)
		{
			var action = args.Action;
			switch (action) {
				case NotifyCollectionChangedAction.Add:
					foreach (var element in args.NewItems.Cast<INotifyPropertyChanged>()) {
						element.PropertyChanged += Element_PropertyChanged;
						
						if (!rowsDict.ContainsKey(element)) {
							var type = element.GetType();
							var table = GetTable(type);
							DataRow row = table.NewRow();
							row.SetField<int>(pref + pnKey, ++counter);

							foreach (PropertyInfo pr in type.GetProperties())
								row[pref + pr.Name] = pr.GetValue(element, null) ?? DBNull.Value;

							rowsDict.Add(element, row);
							table.Rows.Add(row);
							
							Update(type);
						}
					}
					break;
				case NotifyCollectionChangedAction.Remove:
					foreach (var element in args.OldItems.Cast<INotifyPropertyChanged>()) {
						element.PropertyChanged -= Element_PropertyChanged;
						
						var type = element.GetType();
						var table = GetTable(type);
						
						DataRow row = rowsDict[element];
						rowsDict.Remove(element);
						row.Delete();
						
						Update(type);
					}
					break;
			}
		}
		
		private DataTable GetTable(Type type)
		{
			return dataSet.Tables[dictionary[type].TableName];
		}
		
		private void Update(Type type)
		{
			dictionary[type]
				.Adapter
				.Update(GetTable(type));
		}
		
		private void Element_PropertyChanged(object sender, PropertyChangedEventArgs args)
		{
			string pn = args.PropertyName;
			DataRow r = rowsDict[sender];
			PropertyInfo pr = sender.GetType().GetProperty(pn);
			var prValue = pr.GetValue(sender, null);
			
			r[pref + pn] = prValue ?? DBNull.Value;
			Update(sender.GetType());
		}
		
		public ObservableCollection<T> GetCollection<T>() {
			return (ObservableCollection<T>)dictionary[typeof(T)].Collection;
		}

		private static readonly Dictionary<Type, ADOX.DataTypeEnum> ptDict
			= new Dictionary<Type, ADOX.DataTypeEnum>() {
			{ typeof(string), ADOX.DataTypeEnum.adLongVarWChar },//MEMO, RTF
			{ typeof(int), ADOX.DataTypeEnum.adInteger },
			{ typeof(int?), ADOX.DataTypeEnum.adInteger },
			{ typeof(bool), ADOX.DataTypeEnum.adBoolean },//Nullable<Boolean> not supported
			{ typeof(DateTime), ADOX.DataTypeEnum.adDate },
			{ typeof(DateTime?), ADOX.DataTypeEnum.adDate },
			{ typeof(double), ADOX.DataTypeEnum.adDouble },
			{ typeof(double?), ADOX.DataTypeEnum.adDouble }
		};
		
		private static void Create(string dbName, string password, Type type)
		{
			Create(dbName, password, new Type[] { type });
		}
		
		private static void Create(string dbName, string password, Type[] types)
		{
			ADOX.Catalog dbCatalog = new ADOX.CatalogClass();
			dbCatalog.Create(GetConnectionString(dbName, password));
			
			foreach (var type in types) {
				
				string dtName = pref + type.Name;
				
				ADOX.Table adoxTable = new ADOX.TableClass() {
					Name = dtName,
					ParentCatalog = dbCatalog
				};
				
				ADOX.Column adoxKeyColumn = new ADOX.ColumnClass() {
					ParentCatalog = dbCatalog,
					Name = pref + pnKey,
					Type = ADOX.DataTypeEnum.adInteger
				};
				
				adoxKeyColumn.Properties["Nullable"].Value = false;
				
				adoxTable.Columns.Append(adoxKeyColumn, ADOX.DataTypeEnum.adInteger, 0);
				adoxTable.Keys.Append("PrimaryKey", KeyTypeEnum.adKeyPrimary, adoxKeyColumn, dtName, pref + pnKey);
				
				foreach (string pn in type.GetProperties().Select(s => s.Name).Except(new string[] { pnKey })) {
					
					ADOX.DataTypeEnum pt = ptDict[type.GetProperty(pn).PropertyType];
					ADOX.Column adoxColumn = new ADOX.ColumnClass() {
						ParentCatalog = dbCatalog,
						Name = pref + pn,
						Type = pt
					};
					Type t = type.GetProperty(pn).PropertyType;
					bool g = t.IsGenericType && t.GetGenericTypeDefinition() == typeof(Nullable<>);
					adoxColumn.Properties["Nullable"].Value = g || t == typeof(String);
					
					adoxTable.Columns.Append(adoxColumn, pt, 0);
				}
				
				dbCatalog.Tables.Append(adoxTable);
			}
			
			(dbCatalog.ActiveConnection as ADODB.Connection).Close();

			dbCatalog = null;
		}

		private void Open()
		{
			foreach (var element in dictionary) {
				
				var table = dataSet.Tables[element.Value.TableName];
				element.Value.Adapter.Fill(table);

				var type = element.Key;
				var collection = element.Value.Collection;
				var constructor = type.GetConstructor(new Type[] {});
				var properties = type.GetProperties();
				var addMethod = collection
					.GetType()
					.GetMethod("Add", new Type[] { type });
				
				table
					.AsEnumerable()
					.Select(row => {
					        	var item = constructor.Invoke(null);

					        	foreach (var pr in properties) {
					        		var rowValue = row[pref + pr.Name];
					        		pr.SetValue(item, rowValue == DBNull.Value ? null : rowValue, null);
					        	}
					        	
					        	rowsDict.Add(item, row);
					        	return item;
					        })
					.ToList()
					.ForEach(item => addMethod
					         .Invoke(collection, new object[] { item })
					        );
				
				var rows = table.AsEnumerable();
				if (rows.Count() != 0) {
					counter = Math.Max(counter, rows.Max(e => e.Field<int>(pref + pnKey)));
				}
			}
		}
	}
}